﻿// 繁忙的都市.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//


/*
* http://ybt.ssoier.cn:8088/problem_show.php?pid=1392
【题目描述】
城市C是一个非常繁忙的大都市，城市中的道路十分的拥挤，于是市长决定对其中的道路进行改造。
城市C的道路是这样分布的：城市中有n个交叉路口，有些交叉路口之间有道路相连，两个交叉路口之间最多有一条道路相连接。
这些道路是双向的，且把所有的交叉路口直接或间接的连接起来了。每条道路都有一个分值，分值越小表示这个道路越繁忙，越需要进行改造。
但是市政府的资金有限，市长希望进行改造的道路越少越好，于是他提出下面的要求：

1．改造的那些道路能够把所有的交叉路口直接或间接的连通起来。

2．在满足要求1的情况下，改造的道路尽量少。

3．在满足要求1、2的情况下，改造的那些道路中分值最大值尽量小。

作为市规划局的你，应当作出最佳的决策，选择那些道路应当被修建。

【输入】
第一行有两个整数n,m表示城市有n个交叉路口，m条道路。接下来m行是对每条道路的描述，u, v, c表示交叉路口u和v之间有道路相连，分值为c。(1≤n≤300，1≤c≤10000)。

【输出】
两个整数s, max，表示你选出了几条道路，分值最大的那条道路的分值是多少。

【输入样例】
4 5
1 2 3
1 4 5
2 4 7
2 3 6
3 4 8
【输出样例】
3 6
*/

#include <iostream>
#include <algorithm>

using  namespace std;

const int N = 310,M=N * N;
const int INF = 0x3f3f3f3f;
int n, m;       // n是点数，m是边数
int p[N];       // 并查集的父节点数组
int ans;
int cnt;

struct Edge     // 存储边
{
    int a, b, w;

    bool operator< (const Edge& W)const
    {
        return w < W.w;
    }
}edges[M];

int find(int x)     // 并查集核心操作
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}
int kruskal()
{
    sort(edges, edges + m);

    for (int i = 1; i <= n; i++) p[i] = i;    // 初始化并查集

    int res = 0; cnt = 0;
    for (int i = 0; i < m; i++)
    {
        int a = edges[i].a, b = edges[i].b, w = edges[i].w;

        a = find(a), b = find(b);
        if (a != b)     // 如果两个连通块不连通，则将这两个连通块合并
        {
            p[a] = b;
            res += w;
            ans = max(ans, w);
            cnt++;
        }
    }

    if (cnt < n - 1) return INF;
    return res;
}

int main()
{
    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        int a, b, w; cin >> a >> b >> w;
        edges[i].a = a; edges[i].b = b; edges[i].w = w;
    }
    kruskal();
    cout << cnt << " " << ans << endl;


	return 0;
}

 